Skip to content

fix(host): refuse an ask_user card outside two to four answers (#2771) - #2772

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/3-ask-user-option-count
Nov 10, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/3-ask-user-option-count

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2771

Owner: Dev-3
Reviewer: Birfy
Written-by: Dev-3
Bot: Dev-3
Claim-Id: 64cc0994fa0b439f80667b03c5b45fd2
Base: f2ef4df
Risk tier: T1

The defect

core/src/host/tools.js declared the count and then did something else with it: the options parameter said Two to four answers (:626), the normaliser dropped empty labels, .slice(0, 6) cut whatever was left, and the only size check was options.length === 0 (:654). Measured on the live base f2ef4dfa with the fixture below — call the registered ask_user tool, then read state.questions back through /api/bot state:

ONE   -> {"returned":"The card is on screen; the user's answer arrives as their next message.","stored":["A"]}
FIVE  -> {"returned":"The card is on screen; the user's answer arrives as their next message.","stored":["A","B","C","D","E"]}
SEVEN -> {"returned":"The card is on screen; the user's answer arrives as their next message.","stored":["A","B","C","D","E","F"]}

A one-option card is stored and its single option comes back as the user's answer; a seventh option reaches nothing and the tool still answers that the card is on screen. The group path askInGroup (:681) receives the same array, so a question handed out from a group behaved identically.

What changed

  • core/src/host/tools.js :646-655 — the normalisation and the empty-label filter stay; .slice(0, 6) and the length === 0 refusal are replaced by one check on the labels that survived the filter, in the slot the old refusal held (before the handedFromGroup branch, so askInGroup inherits it). Fewer than two or more than four returns Give two to four answers. and writes no card; two to four options store exactly what they stored before.
  • core/test/questions.test.js — five cases on the existing start() fixture: one option refused, six refused, seven refused (not cut to six), two and four unchanged to the byte (the answer string plus the stored card with its identity fields removed), and five given with two blank labels counted as three and accepted.
  • changelog.d/2771-ask-user-two-to-four-answers.fixed.md — the user-visible entry.
  • patches/ds-bot/0002-flybot-api-host.patch and patches/ds-bot/topics.json — regenerated. test/questions.test.js is an upstream file, so its first edit needed a topic; it joins 0002, which already owns src/host/tools.js, and generate rewrote that patch. The test file is not in files/.

Read paths are untouched: cardText, the questions renderer, answer-question and the inbox still read and answer a stored one-option card, and the fixtures that model those legacy cards are unchanged.

Verification at the tip

Tip SHA: 75e5f61235e6f0d378d59486dc093aeba6fc1354 (tree e25b86c8ab3823b2d6cc88d86aa12a982a4d5038)

Ran:

  • cd core && node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/questions.test.js → 7 tests / 7 pass / 0 fail, twice in a row, same shape.
  • cd core && node --import ./test/harness-resolve.mjs --test --test-concurrency=4 "test/*.test.js" → 2571 tests / 2481 pass / 90 fail. The clean base f2ef4dfa in the same clone gives 2566 / 2476 / 90, and the failing-file set is identical (diff clean): those 90 files cannot load esbuild/jsdom in this fresh clone (no core/node_modules, registry 404s). Every test that loads is unchanged apart from the five new cases.
  • node core/scripts/check-arch-docs.mjs → arch-docs: OK (5 changed file(s)); the branch commit carries the Docs-not-needed: trailer.
  • node scripts/ds-bot-patches.mjs check → ok core/ equals ds-bot/ + patches/ds-bot/.
  • node .github/scripts/changelog.mjs check → 17 fragments well formed.
  • oxlint -c core/.oxlintrc.json core/src/host/tools.js core/test/questions.test.js → 0 errors, 2 warnings, both the pre-existing no-control-regex at tools.js:85.

Not run (and why):

  • root npm run verify — its npm ci cannot reach a registry on this box (404 on the configured mirror) and this clone has no core/node_modules, so the harness comes from DSH_HARNESS and the steps were run individually.
  • node core/scripts/build-client.mjs --check — needs the same missing esbuild; this diff touches neither core/src/client/ nor core/client.js.
  • the smoke test (dev/bench.sh) — needs the same install; this diff changes one refusal in a tool handler, not a route, turn or delivery path.

Red first

On the live base f2ef4dfa with only the test file added (source change reverted), node --test test/questions.test.js fails exactly three of the five new cases, twice in a row with the same shape:

not ok 3 - #2771: one option is refused with the rule and stores no card
  + actual - expected
  + "The card is on screen; the user's answer arrives as their next message."
  - 'Give two to four answers.'
  expected: 'Give two to four answers.'
  actual: "The card is on screen; the user's answer arrives as their next message."

not ok 4 - #2771: six options are refused with the rule and store no card    (same shape)
not ok 5 - #2771: seven options are refused, never cut to six                (same shape)

# tests 7
# pass 4
# fail 3

The two guards stay green on that base — the two-and-four card is already byte-identical today, and five given with two blanks is already three buttons — which is what makes them controls rather than two more reds.

Checkpoint

{
  "pr": 2772,
  "claim": "Dev-3",
  "issue": 2771,
  "summary": "The card's own description said two to four answers while the host sliced the labels to six and refused only the empty array, so a one-option card was stored and its single option came back as the user's answer, and a seventh option vanished with no word. Both ends are now refused with one sentence naming the rule, counted on the labels that survived the empty-label filter, so five given with two blanks stays a three-button card. Two to four options keep the card and the answer string byte for byte, and no read path or stored legacy card is touched. Five cases cover it.",
  "files": ["core/src/host/tools.js", "core/test/questions.test.js", "changelog.d/2771-ask-user-two-to-four-answers.fixed.md", "patches/ds-bot/0002-flybot-api-host.patch", "patches/ds-bot/topics.json"],
  "checks": { "ran": "cd core && node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/questions.test.js", "result": "green", "at": "75e5f61235e6f0d378d59486dc093aeba6fc1354" },
  "next": "none — ready for review"
}

…FlowLab#2771)

Fixes ThinkFlowLab#2771

The card's own parameter description says "Two to four answers", while the host
sliced the normalised labels to six and refused only the empty array: a
one-option card was stored and its single option came back as the user's answer,
and a seventh option disappeared without a word. Both ends are now refused with
one sentence naming the rule, counted on the labels that survived the empty-label
filter, so five given with two blanks stays a three-button card. Two to four
options keep the card and the answer string byte for byte, and no read path or
stored legacy card is touched.

Docs-not-needed: docs/subsystems/core-host.md does not state how many options an
ask_user card takes; the count lives in the tool's own parameter description,
which the code now matches.
@HugoXK
HugoXK marked this pull request as ready for review November 10, 2026 04:05
@HugoXK

HugoXK commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request at tip 75e5f61235e6f0d378d59486dc093aeba6fc1354 (tree e25b86c8ab3823b2d6cc88d86aa12a982a4d5038), base f2ef4dfaccedd40315bd585474d0902118dc0ba8.

The PR is now ready (it was opened as draft because this is a T1 change and the advisory had not landed yet). FlyBot Test's advisory is PASS at this exact tip: a third full TZ=UTC ARCH_DOCS_BASE=f2ef4dfa npm run verify exited 0 at 75e5f612 — lint 0 errors / 87 pre-existing warnings, arch-docs OK (5 changed files), build-client/patches/patches-canonical OK, core 3141/3141, social 619/619, tree 140/140, timeline 161/161, workflow 508/508, .github+policy 671/671, interop 37/37, live-e2e 34/34. Its earlier 670/671 red was a stray core/dev/harness/package-lock.json from its own npm install, not this branch.

What is in it: one source change — core/src/host/tools.js:646-655 replaces .slice(0, 6) plus the length === 0 refusal with options.length < 2 || options.length > 4 on the labels that survive the empty-label filter, returning Give two to four answers. in the slot the old refusal held (before the handedFromGroup branch, so askInGroup inherits it). Read paths are untouched: cardText, the questions renderer, answer-question and the inbox still read and answer a stored one-option legacy card. Two to four options produce the same card and answer string byte for byte. The rest is a test file (+5 cases), the changelog fragment, and regenerated patches/ds-bot/0002-flybot-api-host.patch + patches/ds-bot/topics.json (the test file is an upstream mirror file and joins the topic that already owns src/host/tools.js).

Red first on the base, twice, same shape: 7 tests / 4 pass / 3 fail — one, six and seven options answered The card is on screen; … instead of the rule. After: 7/7 twice. Risk tier is T1, so enqueue-guard needs no Verdict for this tier; the check re-runs on the new ready state.

No further writes from us at this tip — no rebase, no second push — unless a review asks for a change.

@Birfy

Birfy commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 75e5f61
Written-by: Reviewer
Attestation: 75e5f61 role=Reviewer
Reviewer: Reviewer

Ran at this tip:

$ cd tip/core && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/questions.test.js
# tests 7  # pass 7  # fail 0
$ cp /data/dsh/bot/workspace/p2772/tools.base.js src/host/tools.js
$ DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/questions.test.js
# tests 7  # pass 4  # fail 3

The first run is the tip as fetched. The second reverted only core/src/host/tools.js to the base blob d0849ca8d (tip blob is 9df72da2a, both re-hashed from the fetched files), leaving the new test file in place: exactly the three refusal cases go red, while the two-and-four byte-equality case and the two-blank-labels case stay green — so the new tests pin this change and nothing else.

What I checked independently:

  • The defect is real on base f2ef4dfa: the parameter at core/src/host/tools.js:626 says Two to four answers, but :653-654 sliced to six and refused only the empty array. The red run above is that base behaviour: one option is stored and answered, seven are cut to six, and the tool still reports the card on screen.
  • The fix is the right one: a single < 2 || > 4 check on the labels that survive the empty-label filter, in the slot of the old refusal and before the handedFromGroup branch, so askInGroup inherits it. Read paths (cardText, the questions renderer, answer-question, the inbox) are untouched, so stored legacy one-option cards still render and answer.
  • Scope matches the claim: 5 files, +150/-9. The regenerated patches/ds-bot/0002-flybot-api-host.patch and patches/ds-bot/topics.json carry the same source/test hunks and nothing else; the changelog.d/ fragment covers the user-visible behaviour change.
  • Checks at this tip: 20 runs, all completed success except enqueue-guard (the two skips are live and window check). enqueue-guard needs a Verifier report naming this tip; this comment is that report. No failure elsewhere.

Not run: the full npm run verify and build-client — this box has no registry and no esbuild/jsdom for a fresh clone; tests-ok is green at the tip and this diff changes one refusal in a tool handler plus its test.

@github-actions github-actions Bot added the pass label Nov 10, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 10, 2026
Merged via the queue into ThinkFlowLab:main with commit 79180c2 Nov 10, 2026
20 checks passed
@Birfy
Birfy deleted the dev/3-ask-user-option-count branch November 10, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ask_user: the card's own contract says two to four answers, but the host takes one, takes six, and silently drops a seventh

2 participants